Skip to content
2797. 带有占位符的部分函数
概述
partial 接受一个原函数 fn 和一个参数数组 args,数组中值为 '_' 的位置代表占位符。返回的新函数在被调用时,会将后续传入的参数按顺序替换所有占位符,如果仍有剩余参数则追加到列表末尾,最后以组装好的完整参数列表调用 fn。
基本概念
部分应用(partial application)与柯里化(currying)都是函数式编程中拆分参数的手段,但行为不同:
- 柯里化:将
f(a, b, c)转换为f(a)(b)(c),每次只接受一个参数。 - 部分应用:固定函数的一部分参数,返回一个接受剩余参数的新函数,不改变函数“元数”(arity)形态。
本题引入占位符,允许在提前绑定的参数中预留空位,由后续调用时的实参填充,不再局限于“只能固定前几个参数”。
工作原理
返回的函数接收到新参数时,遍历 args 数组,每遇到值为 '_' 的元素,就从剩余参数 restArgs 的头部取出一个值进行替换。所有占位符填充完毕后,若 restArgs 中仍有剩余参数,则合并到参数列表末尾。最终使用拼接完成的参数调用 fn。
以下通过一个 TypeScript 实现来观察整个过程:
ts
type JSONValue =
| null
| boolean
| number
| string
| JSONValue[]
| { [key: string]: JSONValue };
type Fn = (...args: JSONValue[]) => JSONValue;
function partial(fn: Fn, args: JSONValue[]): Fn {
return function (...restArgs) {
let _args = args.map((item) => {
if (restArgs.length > 0 && item === '_') {
return restArgs.shift();
}
return item;
});
if (restArgs.length > 0) {
_args = _args.concat(restArgs);
}
return fn(...(_args as JSONValue[]));
};
}shift() 在原数组上移除第一个元素并返回,每替换一个占位符,restArgs 的长度减一。这一副作用正好对应“按顺序一次性消耗”的语义。
基本用法
partial 的第一个参数是函数,第二个参数是数组,数组中可混合固定值与占位符 '_'。返回的函数可以像普通函数一样调用。注意:每次调用返回的函数都会重新消耗参数,不存在累积状态。
ts
function greet(greeting: string, name: string) {
return `${greeting}, ${name}`;
}
const greetHello = partial(greet, ['Hello', '_']);
console.log(greetHello('World')); // "Hello, World"API
函数签名
ts
function partial(fn: Fn, args: JSONValue[]): Fnfn:需要被部分应用的函数,可接受任意数量的JSONValue参数。args:由固定值和占位符'_'组成的数组。- 返回值:一个新函数,签名
(...args: JSONValue[]),返回类型与原函数一致。
示例
以下示例基于上述 partial 实现,在 Node.js 环境下运行。
无占位符
args 中不包含占位符时,返回的函数已经携带了全部参数。后续调用传入的实参会追加到参数列表末尾,但因为 add 只接收前两个参数,多余的参数不会影响计算结果。
ts
function add(a: number, b: number) {
return a + b;
}
const addFivePlusThree = partial(add, [5, 3]);
console.log(addFivePlusThree()); // 8
console.log(addFivePlusThree(10, 20)); // 8,多余的参数被传入但不被 add 使用占位符位于不同位置
占位符可以出现在参数列表的任意位置,后续调用时按声明顺序依次填充。
ts
function introduce(name: string, age: number, city: string) {
return `${name} is ${age} years old and lives in ${city}.`;
}
// 占位符在第一个和第三个位置
const intro1 = partial(introduce, ['_', 25, '_']);
console.log(intro1('Alice', 'Beijing'));
// "Alice is 25 years old and lives in Beijing."
// 占位符连续出现
const intro2 = partial(introduce, ['_', '_', 'Shanghai']);
console.log(intro2('Bob', 30));
// "Bob is 30 years old and lives in Shanghai."多余参数
调用返回函数时提供的实参数量超过占位符数量,多余实参会追加到参数列表末尾。
ts
function sumAll(...nums: number[]) {
return nums.reduce((acc, n) => acc + n, 0);
}
const sumWithThree = partial(sumAll, [1, '_', 3]);
console.log(sumWithThree(2, 4, 5)); // 15,最终参数为 [1, 2, 3, 4, 5]参数不足
调用时传入的参数少于占位符数量,未替换的占位符会以字符串 '_' 的形态直接传入原函数。这通常是调用者的疏忽。
ts
const incomplete = partial(greet, ['_', '_']);
console.log(incomplete('Hey'));
// "Hey, _" —— 占位符 '_' 作为 name 传入注意点
shift()的副作用map回调中调用restArgs.shift()直接缩减了剩余参数数组。虽然这满足了按顺序消耗参数的需求,但如果后续代码仍需要完整的restArgs(例如日志记录或二次处理),副作用会造成数据丢失。替代方案是使用独立索引变量追踪消耗位置,而不修改原数组。占位符使用字符串
'_'的歧义
当用户真实想传递的参数就是字符串'_'时,会产生无法区分的冲突。函数式库(如 Ramda)通常使用R.__这种特殊对象作为占位符,通过引用比对来避免问题。需要更健壮的实现时,可以将占位符声明为唯一的 Symbol 或特定引用值。未替换占位符的处理
如果调用时提供的参数数量不足,占位符会作为字符串字面量传入原函数。这一行为通常不是调用者本意。在实际实现中可以考虑抛出错误,或者保留占位符等待下一次调用(即允许多次部分应用),但这已超出本题规范。
限制
- 仅支持单次调用补全参数,无法实现“再次部分应用”后继续保留未填充占位符的链式调用。
- 参数合并方式仅限于按顺序替换并追加剩余参数,不支持非顺序匹配(如按名称绑定)。
- 占位符判断基于严格相等(
=== '_'),如果原始参数数组中包含与占位符相同的字符串,行为不可预料。 - 返回的函数签名丢失了原函数的参数个数信息,IDE 提示和运行时校验能力较弱。
应用
带有占位符的部分应用可以减少重复代码:
- 提前绑定配置或上下文:将日志函数的前两个参数固定为日志级别和模块名,剩余消息内容由调用者提供。
- 事件处理:在 Node.js 的 EventEmitter 或浏览器事件监听中,可以将事件源、事件类型等固定,仅暴露出事件对象或自定义数据。
- 与数组方法配合:对
map、filter等高阶函数的回调进行参数适配,快速生成适用于不同场景的专用函数。 - 测试辅助:将复杂函数的某些依赖或选项固化,构造出更容易验证的单一行为版本。
